<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* File contains implementation of the Configuration utility class.
* The purpose of this library to handle configuration across the whole system.
*
* @todo Make Configuration serializable/cacheable using lib/cache/Cache
*
* @package common
* @category Configuration
* @version v1.1
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
*
* @PHP5only
* @config define CONFIGBASE
*
* Changelog:
*
*  v1.1
*    - removed support for option queries without module name
*/

if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}

require_once('lib/GenericExceptions.php');
require_once('lib/FileUtils.php');

/**
* Specific for file based drivers.
* Base prefix where all configuration files are located. Trailing slash '/' is required!
* <b>Note</b>: relative path will be resolved from entry file, usually index.php
* Relative path may be used for testing/debuging only, for production use this path MUST be absolute.
*/
define('CONFIGBASE', 'D:/WebHost/eurosimple/data/config/');
//define('CONFIGBASE', '/var/www/web24/html/data/config/');

/**
* Class name of the driver implementing configuration access.
* Do not change.
*/
define('CONFIGDRIVER', 'XMLConfigurationDriver');

/**
* Specific configuration option is not found.
* This exception indicates a missing or not valid configuration option.
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class ConfigurationException extends RuntimeException {
	/**
	 * Config query that caused problem.
     * @var string
     */
    protected $query;

    /**
     * Construct new configuration exception with query that caused the problem.
     * @param string $message the error message
     * @param string $query the config query that has caused the problem
     * @param integer $code optional error code
     */
    public function __construct($message=null, $query, $code=0) {
    	parent::__construct($message, $code);
        $this->query = $query;
    }

    /**
     * Returns config query that has caused the problem
     * @return string non valid or not existing option query
     */
    public function getQuery() {
    	return $this->query;
    }
}

/**
* System wide configuration registry.
*
* Simple in use configuration registry. All options are organized in modules,
* like: db, db/mysql, core/news/plugins etc. Options with equal name may exist as
* long as they reside in diffrent modules. Options and modules are loadable on demand,
* this makes the configuration registry fast and memory efficient.
*
* To query the option a special syntax is used, which may not only contain the option name,
* but also the preffered format. Examples:
* <pre>
*   Configuration::get('[module]my.property', 'default value');
*   Configuration::getOption('[some/module]my.property'); //will raise ConfigurationException if not found
*   Configuration::get('[some/module]int:my.property'); //example of using 'int' filter which ensures the value is integer
*
*   $config = Configuration::instance(); //singleton
*   $config['[module]my.property']; //equals to Configuration::getOption() - required option
*   $config['![module]my.property']; //equals to Configuration::get(), default value is null
*   $config['[some/module]file:my.property'];
* </pre>
*
* To know more about option queries and other advanced features like references read
* the supplied documentation.
*
* @static
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class Configuration implements ArrayAccess {

	/** For ArrayAccess only. Class doesn't have any instance
	* @var Configuration */
	private static $instance = null;
	/** Loaded and resolved option values.
	* @var array */
	private static $values;
	/** All loaded configuration modules.
	* @var array of ConfigurationDriver */
	private static $loaded_modules;
	/** All registered filters.
	* Each filter is a function (string) or an object (ConfigurationFilter)
	* @var array */
	private static $filters;
	/** The root module, which loads all other modules.
	* @var ConfigurationDriver */
	private static $root_driver;

	/** Constructing singleton, you don't need this constructor. */
	private function __construct() {}

	/** Get the configuration object, that may be used as regular associative array.
	* @return Configuration */
	public static function instance() {
		if(is_null(self::$instance)) { //we may not be extended... but in case we do...
			$cn = __CLASS__;
			self::$instance = new $cn();
			self::$loaded_modules = array();
			self::$values=array();

			//init filters
			new ConfigurationBasicFilterSet(self::$instance);
			new ConfigurationFilesFilterSet(self::$instance);

			//init the driver
			$drv = CONFIGDRIVER;
			self::$root_driver = new $drv();
		}
		return self::$instance;
	}

	/**
	* Get the option value.
	*
	* Default value will be used if option is not found. Filters will be evaluated no matter the value
	* is default or not. If one of the filters returns NULL, then evaluation will be stoped and NULL
	* will be returned as result.
	*
	* @param string $name full featured option query
	* @param string $def default value if option is not defined, by default is NULL
	* @param mixed $options additional options for the filters if required
	* @param string $cur_module if set, then '.'(dot) wil be resolved against this module (for internal use only)
	* @return mixed option value as returned by the latest filter
	*/
	public static function get($name, $def=null, $options=null, $cur_module=null) {
		$path = self::parseName($name); //throws SyntaxException
		//$path = [name, module name, filterchain]
		$varname = self::buildName($path, $cur_module);
		$val = self::getOptionValue($path, $varname, $cur_module); //loads on demand, throws critical Exception
		$val = is_null($val)? $def: $val;
		if(is_null($val) || empty($path[2])) return $val; //there is no need for filters

		//apply filter chain
		$fls = explode(':', rtrim($path[2], ':'));
		return self::evaluateFilters($fls, $val, $options);
	}

	/**
	* Get required option.
	*
	* Method works similar to get(string, mixed), but throws exception if option is not found.
	* In most cases it is better to use this method instead of providing the hard coded defaults.
	*
	* <b>Note</b>: method may still return NULL if one of the filters returns NULL.
	*
	* @throws ConfigurationException if option is not found
	* @param string $name full featured option query
	* @param mixed $options additional options for the filters if required
	* @param string $cur_module if set, then '.'(dot) wil be resolved against this module (for internal use only)
	* @return mixed option value as returned by the latest filter
	*/
	public static function getOption($name, $options=null, $cur_module=null) {
		$path = self::parseName($name); //throws SyntaxException
		//$path = [name, module name, filterchain]
		$varname = self::buildName($path, $cur_module);
		$val = self::getOptionValue($path, $varname, $cur_module); //auto loads, throws critical Exception
		if(is_null($val)) throw new ConfigurationException("Option named '{$varname}' is not found!", $varname);
		if(empty($path[2])) return $val; //there is no need for filters

		//apply filter chain
		$fls = explode(':', rtrim($path[2], ':'));
		return self::evaluateFilters($fls, $val, $options);
	}

	/**
	 * Returns option only if $value is null.
	 *
	 * This method will fetch option only if given $value is null, thus configuration option acts
	 * like a default value. Option is always fetched by using <tt>getOption()</tt>, thus exception
	 * will be thrown if option is not defined.
	 *
	 * @throws ConfigurationException if $value is null and option $name is not found
	 * @param mixed $value any value that must not be a null
	 * @param string $name full featured option query
	 * @param mixed $options additional options for the filters if required
	 * @return mixed given $value or fetched configuration option if $value is null
	 */
	public static function byDefault($value, $name, $options=null) {
		return !is_null($value)? $value: self::getOption($name, $options);
	}

	/**
	* Resolve all {%references}
	* The method will replace all option references with their corresponding values.
	*
	* Second parameter allows to specify default values that will be used if option is not found.
	* If $defs is not an array, then given value will be used among all options. Otherwise, if
	* a key equal to reference query exists in given array, then this value will be used for that
	* reference. If no key is defined, then nummeric offset is used, where offset is the position
	* of the reference inside the given string. Examples:
	* <pre>
	*  Configuration::evaluate('My {%[module1]option} and {%[module2]int:other.option}!'); //default is NULL (empty string)
	*  Configuration::evaluate('My {%[module1]option} and {%[module2]int:other.option}!', '{value}'); //default is '{value}' for all references
	*  Configuration::evaluate('My {%[module1]option} and {%[module2]int:other.option}!', array(2, 90)); //option=2, other.option=90
	*  Configuration::evaluate('My {%[module1]option} and {%[module2]int:option}!', array('option'=>2, '[module]option'=>90)); //option=2, [module]option=90
	* </pre>
	*
	* Only the module and option name are used to build the key.
	*
	* <b>Note</b>: reference will only be matched if it follows the syntax rules, otherwise possible
	* reference will be ignored. To explicitly output '{%' use special '{%}' sequence (expl: {%}test} -> {%test})
	*
	* @param string $str_value string value with references
	* @param mixed $defs array of default values
	* @param mixed $options additional options for the filters if required
	* @param string $cur_module if set, then '.'(dot) wil be resolved against this module (for internal use only)
	* @return string resulting string with all options resolved
	*/
	public static function evaluate($str_value, $defs=null, $options=null, $cur_module=null) {
		return preg_replace_callback('/{%((?:\[((?:[a-z0-9\-_]+|\.)(?:\/[a-z0-9\-_]+)*)\])?((?:[a-z0-9_]+:)*)([a-zA-Z0-9\-_]+(?:\.[a-zA-Z0-9\-_]+)*))?}/',
									 array(new ConfigurationReplacer($defs, $options, $cur_module), 'replaceCallback'),
									 $str_value);
	}

	/**
	* Resolve all required {%references}
	* Method equals to Configuration::evaluate() method, but uses <tt>Configuration::getOption()</tt> to get
	* the option value. If one or more references are not resolved, then exception will be thrown.
	*
	* @throws ConfigurationException if one or more references are not found
	* @param string $str_value string value with references
	* @param mixed $options additional options for the filters if required
	* @param string $cur_module if set, then '.'(dot) wil be resolved against this module (for internal use only)
	* @return string resulting string with all options resolved
	*/
	public static function evaluateStrict($str_value, $options=null, $cur_module=null) {
		return preg_replace_callback('/{%((?:\[((?:[a-z0-9\-_]+|\.)(?:\/[a-z0-9\-_]+)*)\])?((?:[a-z0-9_]+:)*)([a-zA-Z0-9\-_]+(?:\.[a-zA-Z0-9\-_]+)*))?}/',
									 array(new ConfigurationReplacer(null, $options, $cur_module), 'replaceCallbackStrict'),
									 $str_value);
	}

	/**
	* Returns the list of all currently loaded modules.
	* This method is for info/debug only.
	* @return array of string where each string is a module-path.
	*/
	public static function listLoadedModules() {
		return array_keys(self::$loaded_modules);
	}

	/**
	* List all currently defined filter names.
	* This method is for info/debug only.
	* @return array of string filter names
	*/
	public static function listDefinedFilters() {
		return array_keys(self::$filters);
	}

	/**
	* List all currently defined option names.
	* The names are full option names without fiter section. If $pattern is a string, then it will
	* be used as is in preg_match to filter out option names. Ensure the module is loaded,
	* otherwise expecting options may not be available!
	*
	* <b>Warning:</b> $pattern is used as is, becuase it is not expected to be provided by the user,
	* instead right pattern must be hardcoded into the module that uses Configuration.
	*
	* @param string $pattern preg_match regular expression, that allows only a particular option names
	* @return array of string option names
	*/
	public static function listDefinedOptions($pattern = null) {
		$ret = array_keys(self::$values);
		if(is_null($pattern)) return $ret;
		foreach ($ret as $k => $value) if(!preg_match($pattern, $value)) unset($ret[$k]);
		return $ret;
	}

//======================- Private part -====================================

	/**
	* Parse full-featured option name (query) to components.
	* @param string $name the full-featured option name
	* @throws InvalidArgumentException if $name doesn't follow syntax rules
	* @return array of query components [option name, module path, filter chain]
	*/
	private static function parseName($name) {
		assert(is_string($name));
		$name = trim($name);
		$mtch = false; //to disable notice
		if(!preg_match('/^(?:\[((?:[a-z0-9\-_]+|\.)(?:\/[a-z0-9\-_]+)*)\])((?:[a-z0-9_]+:)*)([a-zA-Z0-9\-_]+(?:\.[a-zA-Z0-9\-_]+)*)$/', $name, $mtch))
			throw new InvalidArgumentException("Wrong syntax in option name '{$name}'!");
		return array($mtch[3], $mtch[1], $mtch[2]);
	}

	/**
	* Constructs option name from parsed name.
	* Returned name will contain absolute module-path and option name, filters are ignored.
	* @throws InvalidArgumentException if '.' dot is specified, but current module is not
	* @param array $parsed_name parsed option name from parseName() function
	* @param string $cur_module module name-path against which '.' dot will be resolved
	* @return string that can be used as key to find the option value in cached $values array
	*/
	private static function buildName($parsed_name, $cur_module=null) {
		assert(is_array($parsed_name));
		if(!empty($parsed_name[1])) $parsed_name[1]=self::resolveRelativeModule($parsed_name[1], $cur_module);
		//return (!empty($parsed_name[1])? '['.$parsed_name[1].']': '').$parsed_name[0];
		return '['.$parsed_name[1].']'.$parsed_name[0];
	}

	/**
	* Resolve relative (dot) module by given $cur_module.
	* Method returns module-path untouched if path is already absolute.
	* @param string $mod_path module path as returned by parseName()[1]
	* @param string $cur_module module-path agianst which dot should be resolved
	* @return string absolute module path
	*/
	private static function resolveRelativeModule($mod_path, $cur_module) {
		$mod = ltrim($mod_path, '.');
		if(strlen($mod) < strlen($mod_path)) { //resolve dot
			if(!is_string($cur_module) || ($cur_module = trim($cur_module)) == '')
				throw new InvalidArgumentException("Can't resolve local (dot) module, no context is specified! Option: [{$mod_path}]");
			if(!preg_match('/^([a-z0-9\-_]+(?:\/[a-z0-9\-_]+)*)$/', $cur_module)) //check module name
				throw new InvalidArgumentException("Specified conext module-path is of wrong syntax! Path: {$cur_module}");
			$mod_path=$cur_module.(strlen($mod)>0? '/'.$mod: ''); //resolve against parent module
		}
		return $mod_path;
	}

	/**
	* Load options module if it was not loaded before.
	* It is up to the driver which options should be registered. However if
	* $force_export is set, then driver should register all options in the module.
	*
	* <b>Note</b>: registered options will also register default options (without explicit module).
	* This allows a user to query an option without specifying the module, however it is strongly discouraged.
	*
	* @throws Exception on any critical (syntax etc) error
	* @param string $module module name-path
	* @param boolean $force_export if true, then driver will register all options, false - they will be probably registerd on demand
	* @return boolean true if module is loaded or found in cache, false if module is not accessible
	*/
	public static function loadModule($module, $force_export=false) {
		assert(is_string($module) && strlen(trim($module))>0);
		$module = trim($module);
		if(!preg_match('/^([a-z0-9\-_]+(?:\/[a-z0-9\-_]+)*)$/', $module)) //check module name
				throw new InvalidArgumentException("Specified module-path is of wrong syntax! Path: {$module}");

		if(isset(self::$loaded_modules[$module])) return true;
		$mod = self::$root_driver->getChildModule($module); //throws critical Exception
		if(is_null($mod)) return false;
		self::$loaded_modules[$module]=$mod;
		//mdule is available, so no endless recursion will occur
		$mod->registerOptions($force_export);
		return true;
	}

	/**
	* Get the option value.
	* This is macro-like function shared by all get*() methods.
	*
	* @throws RuntimeException on reference recursion
	* @throws Exception on any critical error, usually means the module is damaged.
	* @param array $path parsed option query from parseName()
	* @param string $varname option name from buildName()
	* @param string $cur_module if set, then '.'(dot) wil be resolved against this module
	* @return mixed option value or null if option is not found
	*/
	private static function getOptionValue($path, $varname, $cur_module) {
		if(!isset(self::$values[$varname])) { //try to load option
			assert(!empty($path[1]));
			//if(empty($path[1])) return null; //no module, there is nothing left to do

			//check if we are currently loading this option (recursion)
			if(array_key_exists($varname, self::$values))
				throw new RuntimeException("Option reference recursion detected for option {$varname}!");

			//option must not be marked, because the dirver may reigster another option 'asynchronous',
			//which may require this option. option must be marked after module is loaded and option
			//is still not registered.
			$module = self::resolveRelativeModule($path[1], $cur_module);
			if(!self::loadModule($module)) return null; //treat as 'option not found'

			//module may register the variables, if not query explicitly
			if(!isset(self::$values[$varname]) && !self::$loaded_modules[$module]->emitOption($path[0]))
				return null; //not defined
		}
		return self::$values[$varname];
	}

	/**
	* Define option.
	* This method is for internal use only, it is used by the driver.
	* Method accepts option value and processes all references. This may cause additional
	* driver.emitOption() calls.
	* @throws RuntimeException on recursion
	* @throws Exception on any critical error from other modules
	* @param string $module full module name-path (relative paths are not allowed)
	* @param string $name dot separated option name
	* @param mixed $value option value, all references in strings will be resolved
	* @return boolean true if option is registered, false if it was registered before
	*/
	public static function registerOption($module, $name, $value) {
		$varname = '['.$module.']'.$name;
		if(is_null($value)) throw new InvalidArgumentException("Can't register null as option value {$varname}");
		if(isset(self::$values[$varname])) {
			trigger_error("Option '{$varname}' is already registered!");
			return false; //throw new LogicException("Option {$varname} is already registered!");
		}
		//OK, it is a new option, so mark it and process references
		self::$values[$varname]=null; //is not seen by isset(), but can be detected by array_key_exists()
		//now all recursive calls from getOptionValue() will be detected, so safely evaluate the value

		try {
			//only strings may contain references
			$value = self::evaluateTraversal($value, $module);
		} catch(Exception $e) {
			unset(self::$values[$varname]); //unset loading flag
			throw $e;
		}
		//successfully processed, now value is available for others
		self::$values[$varname]=$value;

		//default values are probably bad thing because of undefined loading order...
		//register default value if it was not registered before
		//if(!isset(self::$values[$name])) self::$values[$name]=$value;
		return true;
	}

	/**
	* Used to traverse multi-dimensional arrays. Recursion is used.
	*/
	private static function evaluateTraversal($value, $module) {
		if(is_string($value)) return Configuration::evaluate($value, null, null, $module);
		elseif(is_array($value)) {
			foreach ($value as $k=>$v) {
				if(is_string($v)) $value[$k]=Configuration::evaluate($v, null, null, $module);
				elseif(is_array($v)) $value[$k]=self::evaluateTraversal($v, $module);
			}
			return $value;
		} else return $value;
	}

	/**
	* Apply a filter to the value.
	* Usually only other filters are interested in this method.
	* <b>Note</b>: it is easy to get endless recursion here!
	* @throws RuntimeException if filter is not defined
	* @param string $filter_name name of the filter to apply
	* @param mixed $value value to pass over the filter
	* @param array $options additional options for the filter
	* @return mixed filtered value
	*/
	public static function applyFilter($filter_name, $value, $options=null) {
		if(!isset(self::$filters[$filter_name])) throw new RuntimeException("Filter '{$filter_name}' is not defined!");
		if(self::$filters[$filter_name] instanceof ConfigurationFilter)
			return self::$filters[$filter_name]->processValue($value, $filter_name, $options);
		else return call_user_func(self::$filters[$filter_name], $value, $filter_name, $options);
	}

	/**
	* Apply set of filters.
	* Method works like <tt>Configuration::applyFilter()</tt> but works with list of filters instead.
	* Method is used internally by <tt>Configuraiton</tt>
	* @throws RuntimeException if filter is not defined
	* @param array $fltrs list of filters
	* @param mixed $value to be filtered
	* @param array $options additional options
	* @return mixed filtered
	*/
	public static function evaluateFilters($fltrs, $value, $options=null) {
		foreach ($fltrs as $fltr) {
			if(!isset(self::$filters[$fltr])) throw new RuntimeException("Filter '{$fltr}' is not defined!");
			if(self::$filters[$fltr] instanceof ConfigurationFilter)
				$value = self::$filters[$fltr]->processValue($value, $fltr, $options);
			else $value = call_user_func(self::$filters[$fltr], $value, $fltr, $options);
			if(is_null($value)) return null;
		}
		return $value;
	}

	/**
	* Register a specific filter.
	*
	* If handler is a string, then it will be used as a regular function. Otherwise it must
	* be the <tt>ConfigurationFilter</tt> object. Three arguments will be provided:
	*  - input value to be processed
	*  - filter name equal to $filter_name, so single handler will be able to handle multiple filters
	*  - options -- array of additional options
	*
	* @throws LogicException if filter is already registered, software bug
	* @param string $filter_name name of the filter as used in the query
	* @param mixed $handler either a string or a ConfigurationFilter object
	*/
	public static function registerFilter($filter_name, $handler) {
		if(isset(self::$filters[$filter_name])) throw new LogicException("Filter named {$filter_name} is already registered");
		if(!is_string($handler) && !is_object($handler)) throw new LogicException('Given handler is of unknown type, only string or ConfigurationFilter object are allowed!');
		if(is_string($handler) && trim($handler)=='') throw new LogicException('"Given function handler is an empty string!');
		if(is_object($handler) && !($handler instanceof ConfigurationFilter)) throw new LogicException("Given object handler doesn't implement ConfigurationFilter interface");
		self::$filters[$filter_name]=$handler;
	}

//======================- ArrayAccess implementation -======================
	/**
	* Get the option by the name/query.
	* If query is prefixed with '!' character, then <tt>Configuration::get()</tt> will be
	* used to retrieve option value. Otherwise <tt>Configuration::getOption()</tt> is used,
	* which will throw exception if option is not defined.
	*
	* @throws SyntaxException if $name doesn't follow syntax rules
	* @throws NotFoundException if option is not found and query is not prefixed by '!'
	* @param string $name full featured option query with '!' prefix extension
	* @return mixed the option value, may be filtered
	*/
	public function offsetGet($name) {
		if($name{0}=='!') return Configuration::get(substr($name, 1));
		else return Configuration::getOption($name);
	}

	/**
	* Check if option is available.
	*
	* By default method tries to load the option if it was not loaded before. However if
	* query is prefixed by '!', then no auto-loading will occur. In other words if query
	* is prefixed by '!' and option is not loaded, then method will return false even if
	* subsequent call to <tt>Configuration::getOption()</tt> will return the value.
	*
	* @throws SyntaxException if $name doesn't follow syntax rules
	* @param string $name full featured option query with '!' prefix extension
	* @return boolean true if option is available
	*/
	public function offsetExists($name) {
		if($name{0}=='!') return isset(self::$values[self::buildName(self::parseName(substr($name, 1)))]);
		//try to load the option
		$path = self::parseName($name);
		$varname = self::buildName($path);
		return !is_null(self::getOptionValue($path, $varname, null));
	}

	/**
	* Throws <b>LogicException</b>, configuration is immutable.
	* @return void
	*/
	public function offsetSet($name, $value) {
		throw new LogicException("Setting the configuration option at run-time is bad idea! Not supported! Name: '{$name}'; value: '{$value}'");
	}

	/**
	* Throws <b>LogicException</b>, configuration is immutable.
	* @return void
	*/
	public function offsetUnset($name) {
		throw new LogicException("Can't unset the option at run-time! Not supported! Name: '{$name}'");
	}
}

/**
* Helper class, used by evaluate() method.
* Since PHP doesn't support closures, we must hold context in something like this.
* This class is for internal use only.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class ConfigurationReplacer {

	/** @var array */
	private $defs;
	/** @var integer */
	private $pos;
	/** @var mixed */
	private $options;
	/** @var string */
	private $cur_module;

	/** Create new replacer with default values. */
	public function __construct($defs, $options, $cur_module) {
		$this->defs = $defs;
		$this->pos=0;
		$this->options=$options;
		$this->cur_module=$cur_module;
	}

	/**
	* Should be called by preg_replace_callback() function.
	* Construct replacement for the reference, default value is used if option is not found.
	* The order of taking default value:
	*  - associative (string) key equal to option name (with optional module-path)
	*  - position offset as offset in given defaults array
	*  - if not $defs is not array, then treat it as default for all references
	*  - use empty string
	* @param array $mth array supplied by preg_replace_callback() function
	* @return string option value
	*/
	public function replaceCallback($mth) {
		if($mth[0]=='{%}') return '{%'; //special '{%}' escape sequence
		$varname = (!empty($mth[2])? '['.$mth[2].']': '').$mth[4];
		if(!is_array($this->defs)) $def=$this->defs;
		elseif(isset($this->defs[$varname])) $def=$this->defs[$varname];
		elseif(isset($this->defs[$this->pos])) $def=$this->defs[$this->pos];
		else $def=null;
		$this->pos += 1;
		return Configuration::get($mth[1], $def, $this->options, $this->cur_module);
	}

	/**
	* Similar to replaceCallback, but uses <tt>Configuration::getOption()</tt>
	* @param array $mth array supplied by preg_replace_callback() function
	* @return string option value
	*/
	public function replaceCallbackStrict($mth) {
		if($mth[0]=='{%}') return '{%'; //special '{%}' escape sequence
		return Configuration::getOption($mth[1], $this->options, $this->cur_module);
	}
}

/**
* Interface definition for a configuration driver.
*
* The driver has a theoretical purpose only, since only XML dirver will
* be used in most cases. This interface defines required low-level functionality.
*
* Instance implementing this interface represent a single module. The first instance
* created by the constructor call without arguments is called root module (driver). Root
* module is used to load additional (child) modules.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
interface ConfigurationDriver {

	/**
	* Register single option.
	* This method will be called only once for the specific $name. Method should
	* register option by calling Configuration::registerOption() if option exists.
	* Otherwise method should return false indicating that option is not defined.
	*
	* @param string $name dot separated name-path.
	* @return boolean true if option is registered, false otherwise
	*/
	public function emitOption($name);

	/**
	* Loads a module.
	* Method is used by the <tt>Configuration:loadModule()</tt> to load a specific module. Only the root module
	* (driver) should implement this method. Configuration provide full '/' separated module name-path,
	* it is up to the driver how to interpret this name-path. The module will be loaded only once,
	* after that it will be cached by <tt>Configuration</tt>.
	*
	* Module doesn't need to actually load any data before first <tt>emitOption()</tt> call.
	* Module must not try to register any option from this method! Subsequent call to
	* registerOptions() is available therefore.
	*
	* @param string $name required '/' (slash) separated module name-path
	* @return ConfigurationDriver loaded module or NULL if module doesn't exist
	*/
	public function getChildModule($name);

	/**
	* Register all available options.
	* Configuration will call this method directly after getting the instance from <tt>root_module.getChildModule()</tt>.
	* Module may register any options using <tt>Configuration::registerOption()</tt>. Once option is registered, no calls to
	* emitOption() with that name will be made.
	*
	* If $force_export is set, then module should register all options even if they where not planed to be registered
	* right now. This may cause additional data loading. This allows a user to explicitly register all default options
	* from the module.
	*
	* <b>Note</b>: while registering options and processing references emitOption() may be called multiple times.
	* This requires arbitrary access to the options. The option may be already registered by one of earlier references.
	* Recursion can't occur, it is checked by <tt>Configuration</tt>.
	* @param boolean $force_export if true, then module should register all avalilable options right now, otherwise module may ignore this call
	* @return void
	*/
	public function registerOptions($force_export=false);
}


/**
* The filter plugin definition.
* Filters are simple classes/functions that process the given value. Resulting value
* may be routed to another filter or returned as result of the query.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
interface ConfigurationFilter {

	/**
	* Transform given $value.
	* Method should recognize given $filter_name and process value according to it. If
	* filter is not recognized, then NULL should be returned. Method will never be invoked
	* with NULL as $value.
	* @throws RuntimeException if filter is not recognized or required option is not provided
	* @param mixed $value value to be processed
	* @param string $filter_name name of the filter without trailing ':'
	* @param array $options associative array with options
	* @return mixed filtered value
	*/
	public function processValue($value, $filter_name, $options=array());
}

/**
* The basic set of filters.
* Supports filters: int, num, str, bool, list.
* Class doesn't has instances, it rather acts as namespace.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class ConfigurationBasicFilterSet implements ConfigurationFilter {

	/** Register this filter by <tt>Configuration</tt>.
	* Called automatically from <tt>Configuration</tt> constructor. */
	public function __construct(Configuration $conf) {
		$conf->registerFilter('int', $this);
		$conf->registerFilter('integer', $this);
		$conf->registerFilter('number', $this);
		$conf->registerFilter('double', $this);
		$conf->registerFilter('float', $this);
		$conf->registerFilter('num', $this);
		$conf->registerFilter('text', $this);
		$conf->registerFilter('str', $this);
		$conf->registerFilter('list', $this);
		$conf->registerFilter('array', $this);
		$conf->registerFilter('bool', $this);
		$conf->registerFilter('boolean', $this);
	}

	/**
	* Transform given $value.
	* Supported filters are: int, num, str, bool, list.
	*
	* If list should be converted to a number or a string, then first element is used as a value. If list is empty, then null
	* will be returned for numbers and empty string for strings.
	* Conversion to boolean recognizes not only usual rules, but also special strings:
	*  - 0, '', array(), '0' -- usual conversion to false
	*  - 'false', 'off', 'no', 'n' -- converts to false
	*  - anything else converts to true
	*
	* List filter recognized option 'list.type' that specifies the value type of all entries in the list.
	* Only: 'int', 'num', 'str', 'list', 'bool' are recognized. On any unknown type the exception will be thrown.
	* Note that list.type list doesn't convert its entries, thus conversion will occur only on a first level.
	*
	* @throws RuntimeException if filter is not recognized or required option is not provided
	* @param mixed $value value to be processed
	* @param string $filter_name name of the filter without trailing ':'
	* @param array $options not used
	* @return mixed filtered value
	*/
	public function processValue($value, $filter_name, $options=null) {
		static $bools = array('false', 'off', 'no', 'n');
		switch ($filter_name) {
			case 'integer':
			case 'int': return is_array($value)? (sizeof($value)>0? intval(current($value)): null): intval($value);
			case 'number':
			case 'double':
			case 'float':
			case 'num': return is_array($value)? (sizeof($value)>0? floatval(current($value)): null): floatval($value);
			case 'string':
			case 'text':
			case 'str': return is_array($value)? (sizeof($value)>0? strval(current($value)): ''): strval($value);
			case 'boolean':
			case 'bool': if(is_array($value)) return sizeof($value) > 0;
						 $value = trim($value);
						 if(empty($value)) return false;
						 foreach ($bools as $bo)
						 	if(strcasecmp($value, $bo) == 0) return false;
						 return true;

			case 'array':
			case 'list': $ret = is_array($value)? $value: array($value);
						 if(isset($options['lsit.type']))
							foreach ($ret as $k=>$val) $ret[$k] = $this->processValue($val, $options['lsit.type']);
						 return $ret;

			default: throw new RuntimeException("Unrecognized filter {$filter_name}!");
		}
	}
}

/**
* File system related filters.
*
* In most cases you want a clearly defined path when you are working with a directory. At least trailing slash
* '/' is (or definitely not) expected. Filters provided by this class can ensure the (system depedent) path is correct.
* Additional info like existence of the file can also be provided.
*
* Filter 'dir'.
* Resolves relative local path to absolute. If directory doesn't exists or path points not to directory, then
* exception will be thrown. Filter accepts option 'base', against which the relative path will be resolved.
* The 'base' is expected to be absolute, if not, it will be resolved against system root. The path will be resolved against
* getcwd() if 'base' is not set.
*
* Returned string will not contain trailing '/' (slash). Directory must be is_readable() but doesn't need to be is_writeable().
* If parameter 'strict' is specified and set to false, then filter will not throw exception if directory from configuration
* tries to point higher than the 'base'. By default 'strict' mode is set to true for security reasons.
*
* Fiter 'file'.
* Works similar to filter 'dir', but ensures the path points to a file. Filter throws exception if file doesn't exist or
* is a directory. File must be is_readable(), but doesn't need to be is_writeable().
* If parameter 'strict' is specified and set to false, then filter will not throw exception if path to file
* from configuration tries to point higher than the 'base'. By default 'strict' mode is set to true for security reasons.
*
* Filter 'path'.
* Combines filters 'dir' and 'file'. If path points to a directory, then 'dir' filter takes action. Otherwise 'file' processes the path.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class ConfigurationFilesFilterSet implements ConfigurationFilter {

	/** Register this filter by <tt>Configuration</tt>.
	* Called automatically from <tt>Configuration</tt> constructor. */
	public function __construct(Configuration $conf) {
		$conf->registerFilter('directory', $this); //absolute path and existence check
		$conf->registerFilter('dir', $this); //absolute path and existence check
		$conf->registerFilter('file', $this); //same as path, but returns null if file doesn't exists
		$conf->registerFilter('path', $this); //completely resolve all .. and ., do not allow non file:// links
	}

	/**
	* Transform given $value.
	* Options used: <b>base</b>, <b>strict</b>.
	*
	* @throws InstallationException if path is not specified or doesn't exist
	* @throws RuntimeException if filter is not recognized or required option is not provided
	* @param mixed $value value to be processed
	* @param string $filter_name name of the filter without trailing ':'
	* @param array $options array with additional (required) options
	* @return mixed filtered value
	*/
	public function processValue($value, $filter_name, $options=null) {
		if(!is_string($value)) //don't even try to understand what input was, it is mission critical info
			throw new InstallationException("Given path '{$value}' is not a path at all!");

		$base = (is_array($options) && isset($options['base']))? $options['base']: null;
		$strict = (is_array($options) && isset($options['strict']))? !empty($options['strict']): true; //strict mode by default
		switch ($filter_name) {
			case 'directory':
			case 'dir': return FileUtils::ensureDir($value, $base, $strict);
			case 'file': return FileUtils::ensureFile($value, $base, $strict);
			case 'path': return FileUtils::ensureExists($value, $base, $strict);
			default: throw new RuntimeException("Unrecognized filter {$filter_name}!");
		}
	}
}

//=============================- Specific implementation -==================================

/**
* XML based implementation.
* This driver loads config properties from XML file. Each module is represented by
* .props file and directory where submodules reside.
*
* XML dictionary:
* <pre>
*  &lt;?xml version="1.0" encoding="UTF-8"?&gt;
*  &lt;config&gt;
*    &lt;!-- Value tag may be omitted. Available types are: string, number, boolean, list --&gt;
*    &lt;option name="dot.separated.name" type="{string(defualt), number, bool, list}"&gt;the value here&lt;/option&gt;
*
*    &lt;!-- type attribute may be omitted too without falling back to default string type --&gt;
*    &lt;option name="dot.separated.name"&gt;
*       &lt;number&gt;90&lt;number&gt;
*    &lt;/option&gt;
*
*    &lt;!-- list is recognized if option contains more than one value tag or single &lt;list&gt; tag or attribute type=list --&gt;
*    &lt;option name="dot.separated.name"&gt;
*        &lt;string&gt;It is list because here are more than one value tag defined&lt;/string&gt;
*        &lt;number&gt;100&lt;/number&gt;
*        &lt;list&gt;Sublist with single string value, becuase string tag may be omitted&lt;/list&gt;
*    &lt;/option&gt;
*    &lt;option name="dot.separated.name"&gt;
*	     &lt;list&gt; &lt;!-- although the value is a single tag list the resulting value will be the list type --&gt;
*            &lt;string&gt;A&lt;/string&gt;
*            &lt;string&gt;B&lt;/string&gt;
*            &lt;string&gt;C&lt;/string&gt;
*        &lt;/list&gt;
*    &lt;/option&gt;
*  &lt;config&gt;
* </pre>
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class XMLConfigurationDriver implements ConfigurationDriver {

	/**
	* XPath access.
	* @var DOMXpath
	*/
	private $access;

	/**
	* Module name, used to resolve local links
	* @var string
	*/
	private $modname;

	/**
	* Construct new module.
	* This constructor should be called without arguments to create new root level module (driver).
	* @param string $name name of the module (for internal use only!)
	*/
	public function __construct($name='') {
		$this->access = null; //module is not found
		if('' == ($this->modname=$name)) return;
		if(!preg_match('/^([a-z0-9\-_]+(?:\/[a-z0-9\-_]+)*)$/', $name)) //check module name
				throw new InvalidArgumentException("Specified conext module-path is of wrong syntax! Path: {$name}");

		//we know for sure $name is safe by regexp
		$path=CONFIGBASE.$name.'.props';
		if(is_file($path) && is_readable($path)) { //load XML
			$dom = new DOMDocument();
			$dom->preserveWhiteSpace = false;
			if(!$dom->load($path)) {
				trigger_error("Syntax error in module {$name}!", E_USER_WARNING);
				return;
			}
			//if(!$dom->schemaValidate(CONFIG_XML_XSD_VALIDATION_FILE)) return; //will trigger some warnings
			$this->access = new DOMXPath($dom);
		}
	}

	/**
	* Register single option.
	* @param string $name dot separated name-path.
	* @return boolean true if option is registered, false otherwise
	*/
	public function emitOption($name) {
		if(is_null($this->access)) return false; //no data loaded

		$option = $this->access->query("/config/option[@name=\"{$name}\"]");
		if($option->length == 0) return false; //such option is not defined
		$option = $option->item(0);
		//let resolve the value
		assert($option->getAttribute('name') == $name); //something wron with XPath then...
		$this->registerOption($name, $option);
		return true;
	}

	/**
	* Construct the value.
	* This function may call itself recursively if option contains a multi-dimensional list.
	*/
	private static function constructValue($nodes) {
		if($nodes->length == 0) return '';
		elseif($nodes->length == 1) //not a list or contains single <list>
			return self::processSingle($nodes->item(0));

		$ret = array();
		foreach ($nodes as $node) {
			if($node->nodeType == XML_ELEMENT_NODE) {
				$ret[]=self::processSingle($node);
			} elseif($node->nodeType == XML_TEXT_NODE || $node->nodeType == XML_CDATA_SECTION_NODE) {
				$v = trim($node->textContent);
				if(!empty($v)) $ret[]=$v;
			} //else ignore anything else
		}
		return $ret;
	}

	/**
	* Parse DOMElement.
	*/
	private static function processSingle($elem) {
		if($elem->nodeType == XML_TEXT_NODE || $elem->nodeType == XML_CDATA_SECTION_NODE) return $elem->textContent;
		elseif($elem->nodeType != XML_ELEMENT_NODE) return '';

		switch($elem->tagName) {
			case 'list':
			case 'L': $val = self::constructValue($elem->childNodes); return is_array($val)? $val: array($val);
			case 'number':
			case 'N': return floatval($elem->textContent);
			case 'boolean':
			case 'B': return self::parseBool($elem->textContent);
			case 'string':
			case 'S': return $elem->textContent;
			default: trigger_error("Unknown value tag: {$elem->tagName}. Ignoring, assuming string!", E_USER_WARNING);
					 return $elem->textContent;
		}
	}

	/**
	* Parse specific boolean value.
	* Values: '', 0, false, off, no, n  -- are false
	*/
	private static function parseBool($str) {
		static $bools = array('false', 'off', 'no', 'n');
		$str = trim($str);
		if(empty($str)) return false;
		foreach ($bools as $bo)
			if(strcasecmp($str, $bo) == 0) return false;
		return true;
	}

	/**
	* Loads a module.
	* Method always returns valid module, even if proprties file doesn't exists.
	*
	* @throws SyntaxException if module contains syntax errors
	* @throws Exception on any critical error
	* @param string $name required '/' (slash) separated module name-path
	* @return ConfigurationDriver possibly loaded module
	*/
	public function getChildModule($name) {
		return new XMLConfigurationDriver($name);
	}

	/**
	* No options will be registered by default.
	* If $force_export is set, then all options will be registered and DOM tree will be disposed.
	*/
	public function registerOptions($force_export=false) {
		if($force_export) {
			$options = $this->access->query("/config/option");
			if($options->length == 0) return; //no options to process;
			foreach ($options as $option) {
				$this->registerOption($option->getAttribute('name'), $option);
			}
		}
	}

	/** Register <option> */
	private function registerOption($name, $option) {
		$option->normalize();
		$val = self::constructValue($option->childNodes);
		$reftype = $option->getAttribute('type');

		if(!is_array($val) && $reftype!='') { //maybe we shal convert string to specific or to list
			switch ($reftype) {
				case 'string': $val = strval($val); break;
				case 'number': $val = floatval($val); break;
				case 'boolean': $val = self::parseBool($val); break;
				case 'list': $val = array($val); break;
				default: trigger_error("Unknown type '{$reftype}'! Ignoring type conversion!", E_USER_WARNING);
			}
		} //else ignore type attribute
		Configuration::registerOption($this->modname, $name, $val);
	}
}



/* ensure init, global instance */
$config = Configuration::instance();
//$config->loadModule('config', true); //first default config

//print_r($config['[config]list:my.property']);
//print_r(Configuration::listDefinedOptions());
//var_dump($config['[config]file:some.file']);
//var_dump($config['[config]dir:some.dir']);
//var_dump($config['[config]path:some.path']);
//var_dump($config['[config]path:some.none']);
?>